#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# Copyright 2002-2023, OpenNebula Project, OpenNebula Systems                #
#                                                                            #
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
# not use this file except in compliance with the License. You may obtain    #
# a copy of the License at                                                   #
#                                                                            #
# http://www.apache.org/licenses/LICENSE-2.0                                 #
#                                                                            #
# Unless required by applicable law or agreed to in writing, software        #
# distributed under the License is distributed on an "AS IS" BASIS,          #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
# See the License for the specific language governing permissions and        #
# limitations under the License.                                             #
#--------------------------------------------------------------------------- #
ONE_LOCATION = ENV['ONE_LOCATION']

if !ONE_LOCATION
    RUBY_LIB_LOCATION = '/usr/lib/one/ruby'
    GEMS_LOCATION     = '/usr/share/one/gems'
    VMDIR             = '/var/lib/one'
    CONFIG_FILE       = '/var/lib/one/config'
else
    RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby'
    GEMS_LOCATION     = ONE_LOCATION + '/share/gems'
    VMDIR             = ONE_LOCATION + '/var'
    CONFIG_FILE       = ONE_LOCATION + '/var/config'
end

# %%RUBYGEMS_SETUP_BEGIN%%
if File.directory?(GEMS_LOCATION)
    real_gems_path = File.realpath(GEMS_LOCATION)
    if !defined?(Gem) || Gem.path != [real_gems_path]
        $LOAD_PATH.reject! {|l| l =~ /vendor_ruby/ }

        # Suppress warnings from Rubygems
        # https://github.com/OpenNebula/one/issues/5379
        begin
            verb = $VERBOSE
            $VERBOSE = nil
            require 'rubygems'
            Gem.use_paths(real_gems_path)
        ensure
            $VERBOSE = verb
        end
    end
end
# %%RUBYGEMS_SETUP_END%%

$LOAD_PATH << RUBY_LIB_LOCATION

require 'pathname'

require_relative 'opennebula_vm'
require_relative '../lib/command'
require_relative '../lib/xmlparser'

include VirtualMachineManagerKVM
include Command

load_remote_env

# ------------------------------------------------------------------------------
# HELPER FUNCTIONS.
#   Note input parameters are defined as instance variables
#     - @deploy_id
#     - @dst_host
#     - @vm_dir
#     - @shared
# ------------------------------------------------------------------------------

# sync paths to the destination VM folder
#
# @param paths [Array/String] Array of paths to sync
#
def rsync_paths(paths, raise_error = true)
    return if paths.empty?

    opts  = '-az'
    paths = paths.join(' ') if paths.class == Array
    dpath = "#{@dst_host}:#{@vm_dir}/"

    tini = Time.now

    rc, _o, e = Command.execute_log("rsync #{opts} #{paths} #{dpath}")

    STDERR.puts "rsync time #{Time.now-tini}s"

    raise StandardError, "Cannot rsync files: #{e}" if rc != 0 && raise_error
end

# In case of error this function is used to remove migration leftovers. For non
# shared storage configuration it will remove the destination VM folder
#
# @param kvm_vm [KvmDomain] libvirt domain class
# @param error[String] message for the StandardError raised by the function
#
def cleanup_host(kvm_vm, error)
    kvm_vm.destroy @dst_host

    kvm_vm.undefine @dst_host

    if !@shared
        Command.ssh(:host => @dst_host, :cmds => "rm -rf #{@vm_dir}")
    end

    raise StandardError, error
end

# Migrate VMs running on local storage, using the copy-storage feature of libvirt
# Disks are scanned and classified as:
#   - Regular disks are copied during migration (devs array). A place holder needs
#     to be created in the destination host
#
#   - Readonly disks are copied before starting the migration (pre_sync array)
#
#   - Snapshots and other ancialliary files are also copied in the pre_sync phase
#
#   - Network disks are assumed to be shared and not copied
#
#   - qcow2 disks with system snapshots are rsync after migration to transfer
#     the snapshots (wiped out during the migration)
#
# To allow this post sync phase the VM is paused after migration (--suspend)
#
# @param kvm_vm [KvmDomain] libvirt domain class
def local_migration(kvm_vm)
    devs = []

    pre_sync  = ["#{@vm_dir}/*.xml"]
    post_sync = []

    kvm_vm.disks.each do |disk|
        dev  = disk[0]
        path = disk[1]

        if !File.symlink? path # qcow2 & raw disks, regular files
            qimg = QemuImg.new path

            format = qimg['format']
            size   = qimg['virtual-size']
            snaps  = qimg['snapshots'] && !qimg['snapshots'].empty?

            if format == 'raw' && kvm_vm.readonly?(path)
                pre_sync << path
            else
                devs << dev
                post_sync << path if format == 'qcow2' && snaps

                cmds =<<~EOS
                    mkdir -p #{File.dirname(path)}
                    qemu-img create -f #{format} #{path} #{size}
                EOS

                Command.ssh(:host => @dst_host, :cmds => cmds,
                            :emsg => 'Cannot create disk')
            end
        elsif path.match(%r{disk.[0-9]*.snap/}) # qcow2-symlink, replica
            devs << dev
            # else
            # network-disk, symlinks are assumed to be network disks
        end

        # Add disk snapshots dir to the list of paths to sync
        if File.directory? "#{path}.snap"
            pre_sync << Pathname.new("#{path}.snap").cleanpath
        elsif (m = path.match(%r{(disk.[0-9]*.snap)/})) # replica
            pre_sync << Pathname.new("#{@vm_dir}/#{m[1]}").cleanpath
        end

        # recreate disk symlinks
        if File.symlink? path
            target = File.readlink(path)
            lname  = path
        elsif (m = path.match(%r{(disk.([0-9]*).snap/.*)}))
            target = m[1]
            lname  = "disk.#{m[2]}"
        else
            next
        end

        cmds =<<~EOS
            cd #{@vm_dir}
            [ -L "#{lname}" ] || ln -s "#{target}" "#{lname}"
        EOS

        Command.ssh(:host => @dst_host, :cmds => cmds,
                    :emsg => 'Cannot symlink disk')
    end

    rsync_paths(pre_sync)

    rc, _out, err = kvm_vm.live_migrate_disks(@dst_host, devs)

    cleanup_host(err) if rc != 0

    rsync_paths(post_sync)

    kvm_vm.resume(@dst_host)
end

# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------

begin
    @deploy_id = ARGV[0]
    @dst_host  = ARGV[1]

    action_xml = XMLElement.new_s(STDIN.read)

    @vm_dir = Pathname.new(action_xml['/VMM_DRIVER_ACTION_DATA/DISK_TARGET_PATH']).cleanpath
    @shared = action_xml['/VMM_DRIVER_ACTION_DATA/DATASTORE/TEMPLATE/SHARED'].casecmp('YES') == 0

    kvm_vm  = KvmDomain.new(@deploy_id)

    # Migration can't be done with domain snapshots, drop them first
    kvm_vm.snapshots_delete

    # Migrate VMs using shared/local storage
    if @shared
        rc, _out, err = kvm_vm.live_migrate(@dst_host)

        cleanup_host(kvm_vm, err) if rc != 0
    else
        local_migration(kvm_vm)
    end

    # Redefine system snapshots on the destination libvirtd
    kvm_vm.snapshots_redefine(@dst_host, @vm_dir)

    # Sync guest time
    if ENV['SYNC_TIME'].upcase == 'YES'
        cmds =<<~EOS
            (
              for I in $(seq 4 -1 1); do
                if #{virsh} --readonly dominfo #{@deploy_id}; then
                  #{virsh} domtime --sync #{@deploy_id} && exit
                  [ "\$I" -gt 1 ] && sleep 5
                else
                  exit
                fi
              done
            ) &>/dev/null &
        EOS

        rc, _o, e = Command.ssh(:host => @dst_host, :cmds => cmds, :emsg => '')

        STDERR.puts "Failed to synchronize VM time: #{e}" if rc != 0
    end

    # Compact memory
    # rubocop:disable Layout/LineLength
    if ENV['CLEANUP_MEMORY_ON_STOP'].upcase == 'YES'
        `(sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null &`
    end
    # rubocop:enable Layout/LineLength
rescue StandardError => e
    STDERR.puts "Error mirgating VM #{@deploy_id} to host #{@dst_host}: #{e.message}"
    STDERR.puts e.backtrace.to_s
    exit(1)
end
